# -*- coding: utf-8 -*-
# This code written by ChanGOGOGO
"""
callplus日报推送
"""
import json
import logging
import decimal
import os
import time
from logging.handlers import RotatingFileHandler
import requests
import pymysql
import yaml
from elasticsearch import Elasticsearch
from apscheduler.schedulers.blocking import BlockingScheduler


def real_round(float_num: float, ndigits: int = 2):
    """
    浮点数四舍五入(避开round函数的坑)
    :param float_num: 浮点数
    :param ndigits: 小数位精确度
    :return: 四舍五入后的浮点数
    """
    if ndigits == 0:
        num_template = "0"
    else:
        num_template = "0." + ("0" * ndigits)
    decimal.getcontext().rounding = "ROUND_HALF_UP"
    precision_num = decimal.Decimal(str(float_num)).quantize(
        decimal.Decimal(num_template)
    )  # 需要把数字转换成字符串才能处理
    return float(precision_num)


def get_today_date():
    today = time.strftime("%Y-%m-%d", time.localtime())
    return today


def get_now_month():
    now_month = time.strftime("%Y%m", time.localtime())
    return now_month


def get_start_end_time(target_date):
    """获取日期的开始结束时间"""
    start_time = f"{target_date} 00:00:00"
    end_time = f"{target_date} 23:59:59"
    return start_time, end_time


def str_contain_keyword(target_str, keywords):
    """"""
    if target_str:
        for keyword in keywords:
            result = target_str.find(keyword)
            if result != -1:
                return True
    return False


def wecom_push_msg(url, msg):
    """向企微群推送消息"""
    data = {"msgtype": "markdown", "markdown": {"content": msg}}
    logging.info(f"向企微群推送消息接口发起请求：\n{data2beautify(data)}")
    res = requests.post(url, data=json.dumps(data)).json()
    logging.info(f"收到企微群推送消息接口响应：\n{data2beautify(res)}")
    return res


def data2beautify(data):
    """
    字典或列表类型的数据，进行美化输出
    :param data: 字典或列表类型数据
    :return: 格式化后的字符串表示
    """
    if not isinstance(data, (dict, list, tuple)):
        raise TypeError("输入必须是字典/列表/元组类型")

    try:
        formatted_data = json.dumps(data, indent=4, ensure_ascii=False)
    except (TypeError, ValueError) as e:
        raise ValueError(f"无法格式化数据: {e}")

    return formatted_data


def setup_logging(log_level: str = "INFO", write_log: bool = True):
    """
    日志格式化打印
    :param log_level: 打印高于等于设定级别的日志。DEBUG < INFO < WARNING < ERROR < CRITICAL
    :param write_log: 是否需要将日志写入到文件
    :return: None
    """
    # 创建日志器
    logger = logging.getLogger()
    logger.setLevel(log_level)  # 设置日志级别为DEBUG
    # 设置日志格式
    formatter = logging.Formatter(
        "%(asctime)s [%(levelname)s]：%(message)s", datefmt="%Y-%m-%d %H:%M:%S"
    )
    # 创建一个流处理器，输出到控制台
    console_handler = logging.StreamHandler()
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)
    # 根据参数决定是否也将日志写入文件
    if write_log:
        # 确保日志目录存在
        if not os.path.exists("logs"):
            os.makedirs("logs")
        # 创建一个循环文件处理器，自动根据大小切割
        file_handler = RotatingFileHandler(
            "logs/info.log", maxBytes=1048576, backupCount=5
        )
        file_handler.setFormatter(formatter)
        logger.addHandler(file_handler)


setup_logging()


def calc_percent(numerator, denominator, reserved_bits=2):
    """保留两位小数，四舍五入"""
    try:
        percent = (numerator / denominator) * 100
        return f"{percent:.{reserved_bits}f}%"
    except ZeroDivisionError:
        return "0.00%"


class DailyReport:
    def __init__(self, env, today=None):
        """"""
        self.env = env
        self.public_config = self.read_config(self.env, "public.yaml")
        self.business_config = self.read_config(self.env, "business.yaml")
        self.robot_url = self.public_config.get("robot_url")
        self.fandeng_url = self.public_config.get("fandeng_url")
        self.fandeng_company_id = self.public_config.get("fandeng_company_id")
        db_ip = self.public_config.get("db_ip")
        db_port = self.public_config.get("db_port")
        db_username = self.public_config.get("db_username")
        db_password = self.public_config.get("db_password")
        self.conn = pymysql.connect(
            host=db_ip,
            port=db_port,
            user=db_username,
            passwd=db_password,
            charset="utf8",
        )
        self.cursor = self.conn.cursor()
        es_ip = self.public_config.get("es_ip")
        es_port = self.public_config.get("es_port")
        self.es = Elasticsearch(
            hosts=[f"http://{es_ip}:{es_port}"],
            headers={"Content-Type": "application/json"},
        )
        self.exclude_company_ids = self.public_config.get("exclude_company_ids")
        # (默认)开场节点关键字
        self.default_start_node_keywords = self.public_config.get(
            "default_start_node_keywords"
        )
        # (默认)通用成功节点关键字
        self.default_success_node_keywords = self.public_config.get(
            "default_success_node_keywords"
        )
        # (默认)通用成功意向关键字
        self.default_success_intent_keywords = self.public_config.get(
            "default_success_intent_keywords"
        )
        # 获取统计日期的起止时间
        if today:
            self.today = today
        else:
            self.today = get_today_date()
        self.start_time, self.end_time = get_start_end_time(self.today)
        self.this_month = get_now_month()

    @staticmethod
    def read_config(env, file_name):
        """读取config.yaml配置文件"""
        config_path = rf"./config/{env}_{file_name}"
        with open(config_path, encoding="utf-8") as f:
            config = yaml.safe_load(f)
        return config

    def get_scene_indicators(self, company_id, business_id, scene_name):
        """
        获取场景指标

        根据公司ID、业务ID和场景名称，获取特定场景的指标配置
        如果特定场景的配置不存在，则逐级回退到默认配置

        参数:
        company_id (str): 公司ID
        business_id (str): 业务ID
        scene_name (str): 场景名称

        返回:
        dict: 特定场景或默认场景的指标配置
        """
        # 默认场景指标，用于在未找到特定配置时返回
        default_scene_indicators = {
            "start_node_keywords": self.default_start_node_keywords,
            "success_node_keywords": self.default_success_node_keywords,
            "success_intent_keywords": self.default_success_intent_keywords,
        }

        try:
            # 如果业务配置存在
            if self.business_config:
                # 尝试获取公司级别的指标配置
                company_indicators = self.business_config.get(company_id)
                # 如果公司级别配置不存在或为空，则返回默认场景指标
                if not company_indicators:
                    return default_scene_indicators

                # 尝试获取业务级别的指标配置
                business_indicators = company_indicators.get(business_id)
                # 如果业务级别配置不存在或为空，则返回默认场景指标
                if not business_indicators:
                    return default_scene_indicators

                # 尝试获取特定场景的指标配置
                scene_indicators = business_indicators.get(scene_name)
                # 如果特定场景配置不存在
                if scene_indicators is None:
                    # 尝试获取该业务下所有场景的通用配置
                    all_scene_indicators = business_indicators.get("all_scene")
                    # 如果通用配置存在，则返回通用配置
                    if all_scene_indicators:
                        return all_scene_indicators
                    # 如果通用配置也不存在，则返回默认场景指标
                    else:
                        return default_scene_indicators

                # 如果找到了特定场景的配置，则返回
                return scene_indicators
            # 如果业务配置整体为空，则返回默认场景指标
            return default_scene_indicators

        except Exception as e:
            # 在发生异常时记录错误日志，并返回默认场景指标
            logging.error(f"获取场景指标时发生错误: {e}")
            return default_scene_indicators

    def es_index_exists(self, es_index):
        """验证es索引是否存在"""
        if self.es.indices.exists(index=es_index):
            return 1
        else:
            return 0

    def query_es(self, es_index, body, source, scroll="1m"):
        # logging.info(f"向Elasticsearch的{es_index}发起查询请求：\n{data2beautify(body)}")
        query = self.es.search(
            index=es_index, body=body, _source=source, scroll=scroll, size=10000
        )
        results = query["hits"]["hits"]  # es查询出的结果第一页
        total_count = query["hits"]["total"]  # es查询出的结果总量
        data = list()
        if total_count >= 1:
            scroll_id = query["_scroll_id"]  # 游标用于输出es查询出的所有结果
            scroll_num = int(total_count / 10000)
            for i in range(0, scroll_num + 1):
                # scroll参数必须指定否则会报错
                query_scroll = self.es.scroll(scroll_id=scroll_id, scroll=scroll)[
                    "hits"
                ]["hits"]
                results += query_scroll
            for result in results:
                source_data = result.get("_source")
                data.append(source_data)
            self.es.clear_scroll(scroll_id=scroll_id)  # 清除滚动会话
        return data

    def query_business(self):
        """查询所有的业务信息：company_id、business_id"""
        if self.exclude_company_ids:  # 需要剔除一些不参与统计的company_id
            exclude_company_ids_len = len(self.exclude_company_ids)
            if exclude_company_ids_len == 1:
                exclude_company_condition = (
                    f"and a.company_id != {self.exclude_company_ids[0]}"
                )
            else:
                new_exclude_company_ids = list(map(str, self.exclude_company_ids))
                exclude_company_condition = (
                    f"and a.company_id not in ({','.join(new_exclude_company_ids)})"
                )
        else:
            exclude_company_condition = ""
        sql = f"""select * from (select a.company_id,b.bid from yicall_business.yicall_aiforce_user a RIGHT JOIN 
        yicall_business.yicall_aiforce_instance b on a.company_id=b.company_id where a.company_id>=1000
        {exclude_company_condition} ORDER BY a.company_id) temp GROUP BY company_id,bid;"""
        logging.info(f"查询所有的业务信息SQL：\n{sql}")
        self.cursor.execute(sql)
        company_business_id = self.cursor.fetchall()
        logging.info(f"查询到各账号下业务信息：\n{company_business_id}")
        company_business_info = list()
        for x in company_business_id:
            company_id, business_id = x
            sql = f"""select pid,business_id,business_name from yicall_{company_id}.call_business 
            where pid={company_id} and business_id={business_id};"""
            self.cursor.execute(sql)
            result = self.cursor.fetchall()
            company_business_info.append(result[0])
        return company_business_info

    def query_scene(self, company_id, business_id):
        """查询场景信息"""
        sql = f"""select scene_id,scene_name from yicall_{company_id}.b_{business_id}_call_scene 
        order by scene_id desc;"""
        self.cursor.execute(sql)
        result = self.cursor.fetchall()
        scene_info = {}
        if len(result) > 0:
            scene_info = {key: value for key, value in result}
        return scene_info

    def close_db(self):
        self.cursor.close()
        self.conn.close()

    def query_callout_info(self, company_id, business_id):
        """查询呼出通话记录信息"""
        es_index = (
            f"yicall_{company_id}_{business_id}_callout_session_ach_{self.this_month}"
        )
        body = {
            "query": {
                "range": {
                    "calloutFinishTime": {"gte": self.start_time, "lte": self.end_time}
                }
            }
        }
        source = [
            "dialogDuration",
            "sceneId",
            "calloutTimes",
            "calloutConnectStatus",
            "calloutLastNodeName",
            "dialogBotTurnCount",
            "dialogCustomerTurnCount",
            "sessionAnalyseInfo",
            "customerCaseId",
        ]
        result = self.es_index_exists(es_index)
        if result == 0:
            return []
        else:
            es_data = self.query_es(es_index, body, source)
            return es_data

    def query_callin_info(self, company_id, business_id):
        """查询呼入通话记录信息"""
        es_index = f"yicall_{company_id}_{business_id}_callout_session_in_ach_{self.this_month}"
        body = {
            "query": {
                "range": {
                    "calloutFinishTime": {"gte": self.start_time, "lte": self.end_time}
                }
            }
        }
        source = [
            "dialogDuration",
            "sceneId",
            "calloutTimes",
            "calloutConnectStatus",
            "calloutLastNodeName",
            "dialogBotTurnCount",
            "dialogCustomerTurnCount",
            "sessionAnalyseInfo",
            "customerCaseId",
        ]
        result = self.es_index_exists(es_index)
        if result == 0:
            return []
        else:
            es_data = self.query_es(es_index, body, source)
            return es_data

    def handle_es_data(self, company_id, business_id):
        """处理从es中查询出来的数据"""
        call_datas = self.query_callout_info(company_id, business_id)
        callin_datas = self.query_callin_info(company_id, business_id)
        # 查询数据库中该业务的场景信息
        scene_info = self.query_scene(company_id, business_id)
        call_datas.extend(callin_datas)
        # 定义场景拨打统计信息
        scene_call_infos = dict()
        for data in call_datas:
            dialog_duration = int(data.get("dialogDuration"))
            # 10秒挂断数
            if 0 < dialog_duration < 10000:
                ten_second_hangup_count = 1
            else:
                ten_second_hangup_count = 0
            # 一通电话的总对话轮次数
            bot_turn_count = int(data.get("dialogBotTurnCount"))
            customer_turn_count = int(data.get("dialogCustomerTurnCount"))
            dialog_turn_count = bot_turn_count + customer_turn_count
            # calloutTimes为1则作为一个有效名单
            callout_times = data.get("calloutTimes")
            if callout_times == "1":
                phone_count = 1
            else:
                phone_count = 0
            # 接通状态
            callout_connect_status = data.get("calloutConnectStatus")
            if callout_connect_status == "100":
                connected_count = 1
            else:
                connected_count = 0
            """获取场景开场节点、成功节点、成功意向的统计指标"""
            scene_id = data.get("sceneId")
            scene_name = scene_info.get(int(scene_id))
            # 查询该业务的场景统计指标
            scene_indicators = self.get_scene_indicators(
                company_id, business_id, scene_name
            )
            start_node_keywords = scene_indicators.get("start_node_keywords")
            success_node_keywords = scene_indicators.get("success_node_keywords")
            intent_keywords = scene_indicators.get("success_intent_keywords")
            user_prefer_keywords = intent_keywords.get("user_prefer")
            special_rule_keywords = intent_keywords.get("special_rules")
            # 收集开场挂断和成功挂断节点数量
            hangup_node = data.get("calloutLastNodeName")
            start_hangup_count = 0
            success_hangup_count = 0
            # 挂断节点中包含开场节点关键字
            if str_contain_keyword(hangup_node, start_node_keywords):
                start_hangup_count = 1
            # 挂断节点中包含成功节点关键字
            if str_contain_keyword(hangup_node, success_node_keywords):
                success_hangup_count = 1
            # 收集成功意向数量
            session_analyse_info = data.get("sessionAnalyseInfo")
            user_intent_count = 0
            if session_analyse_info:
                user_prefer = session_analyse_info.get("dialogUserPrefer")
                if user_prefer_keywords:
                    if str_contain_keyword(user_prefer, user_prefer_keywords):
                        user_intent_count = 1
                else:  # 没有客户意向关键字的话，就用特殊分析规则输出项去计算成功意向
                    if special_rule_keywords:
                        for special_rule_keyword in special_rule_keywords:
                            special_prefer = session_analyse_info.get(
                                special_rule_keyword
                            )
                            if special_prefer:
                                user_intent_count = 1
                                break
            """各统计指标数量进行累积"""
            find_scene = scene_call_infos.get(scene_name, -1)
            if find_scene == -1:
                scene_call_infos[scene_name] = {
                    "total_connected": connected_count,
                    "total_dialog_duration": dialog_duration,
                    "total_dialog_turn": dialog_turn_count,
                    "total_phone": phone_count,
                    "total_success_intent": user_intent_count,
                    "total_ten_second_hangup": ten_second_hangup_count,
                    "total_start_hangup": start_hangup_count,
                    "total_success_hangup": success_hangup_count,
                }
            else:
                scene_call_infos[scene_name]["total_connected"] += connected_count
                scene_call_infos[scene_name]["total_dialog_duration"] += dialog_duration
                scene_call_infos[scene_name]["total_dialog_turn"] += dialog_turn_count
                scene_call_infos[scene_name]["total_phone"] += phone_count
                scene_call_infos[scene_name][
                    "total_success_intent"
                ] += user_intent_count
                scene_call_infos[scene_name][
                    "total_ten_second_hangup"
                ] += ten_second_hangup_count
                scene_call_infos[scene_name]["total_start_hangup"] += start_hangup_count
                scene_call_infos[scene_name][
                    "total_success_hangup"
                ] += success_hangup_count
        logging.info(
            f"yicall_{company_id}下的{business_id}业务在{self.start_time}至{self.end_time}之间各场景的拨打统计数据：\n{scene_call_infos}"
        )
        return scene_call_infos

    def generate_msg(self, company_id, business_id, business_name):
        scene_call_infos = self.handle_es_data(company_id, business_id)
        global global_msg_index
        global fandeng_msg_index
        single_business_msgs = list()
        if scene_call_infos:  # 业务存在场景拨打数据
            for scene_name, scene_call_info in scene_call_infos.items():
                single_scene_msg = list()
                if company_id == self.fandeng_company_id:
                    msg_index = fandeng_msg_index
                else:
                    msg_index = global_msg_index
                single_scene_msg.append(
                    f"\n<font color=green>{msg_index}. {business_name}：{scene_name}</font>"
                )
                # 计算名单接通率
                total_phone = scene_call_info.get("total_phone")
                # 如果某场景呼叫名单数为0，后续操作不做了，也就不推送这个参加的统计信息
                if total_phone == 0:
                    continue
                single_scene_msg.append(f"> 呼叫名单：{total_phone}")
                total_connected = scene_call_info.get("total_connected")
                single_scene_msg.append(f"> 接通量：{total_connected}")
                lists_connected_rate = calc_percent(total_connected, total_phone)
                single_scene_msg.append(f"> 名单接通率：{lists_connected_rate}")
                # 计算平均通话时长
                total_dialog_duration = scene_call_info.get("total_dialog_duration")
                total_connected = scene_call_info.get("total_connected")
                if total_dialog_duration:
                    avg_dialog_duration = int(
                        real_round((total_dialog_duration / total_connected / 1000), 0)
                    )
                else:
                    avg_dialog_duration = 0
                single_scene_msg.append(f"> 平均通时(秒)：{avg_dialog_duration}")
                # 计算平均通话轮次
                total_dialog_turn = scene_call_info.get("total_dialog_turn")
                if total_dialog_turn:
                    avg_dialog_turn = total_dialog_turn / total_connected
                else:
                    avg_dialog_turn = 0
                single_scene_msg.append(f"> 平均轮数：{real_round(avg_dialog_turn, 1)}")
                # 计算10秒挂断率
                total_ten_second_hangup = scene_call_info.get("total_ten_second_hangup")
                ten_second_hangup_rate = calc_percent(
                    total_ten_second_hangup, total_connected
                )
                single_scene_msg.append(f"> 10秒挂断率：{ten_second_hangup_rate}")
                # 开场挂断率
                total_start_hangup = scene_call_info.get("total_start_hangup")
                if isinstance(total_start_hangup, int):
                    start_hangup_rate = calc_percent(
                        total_start_hangup, total_connected
                    )
                else:
                    start_hangup_rate = "<font color=red> - </font>"
                single_scene_msg.append(f"> 开场挂断率：{start_hangup_rate}")
                # 意向率
                total_success_intent = scene_call_info.get("total_success_intent")
                if isinstance(total_success_intent, int):
                    success_intent_rate = calc_percent(
                        total_success_intent, total_connected
                    )
                else:
                    success_intent_rate = "<font color=red> - </font>"
                single_scene_msg.append(f"> 接通意向率：{success_intent_rate}")
                # 接通成功率
                total_success_hangup = scene_call_info.get("total_success_hangup")
                if isinstance(total_success_hangup, int):
                    success_hangup_rate = calc_percent(
                        total_success_hangup, total_connected
                    )
                else:
                    success_hangup_rate = "<font color=red> - </font>"
                single_scene_msg.append(f"> 成功节点人数：{total_success_hangup}")
                single_scene_msg.append(f"> 接通成功率：{success_hangup_rate}")
                global_msg_index += 1
                if company_id == self.fandeng_company_id:
                    fandeng_msg_index += 1
                single_business_msgs.append("\n".join(single_scene_msg))
            return single_business_msgs


def send_msg(url, msg_list, max_len=2048):
    """
    分割消息列表并发送。

    根据最大长度限制，将消息列表中的消息合并成多个字符串并发送。每个字符串的长度不超过最大长度限制。

    参数:
    url (str): 发送消息的URL。
    msg_list (list): 消息列表，包含多个消息字符串。
    max_len (int): 最大消息长度。
    """
    if len(msg_list) == 1:  # 消息中只有一个标题
        msg_list.append("<font color=red>大吃一🐳，今日没有外呼</font>")
        wecom_push_msg(url, "\n".join(msg_list))
    else:
        # 初始化当前字符串
        current_str = ""
        # 遍历消息列表
        for item in msg_list:
            # 如果拼接后的消息长度超过最大长度限制，则发送当前消息字符串，并重新开始拼接
            if len(current_str) + len(item) + 1 > max_len:  # 1是换行符\n的长度
                wecom_push_msg(url, current_str)
                current_str = item
            else:
                # 继续拼接消息
                if current_str:
                    current_str += "\n"
                current_str += item
        if current_str:
            # 发送剩余的消息字符串
            wecom_push_msg(url, current_str)


global_msg_index = 1
fandeng_msg_index = 1


def everyday_push(env, target_date=None):
    daily_report = DailyReport(env, target_date)
    business_list = daily_report.query_business()
    title = f"** <font color=blue>【AI营销运营日报({daily_report.today})】</font>**"
    global_msg_list = list()
    global_msg_list.append(title)
    fandeng_msg_list = list()
    fandeng_msg_list.append(title)
    global global_msg_index
    global fandeng_msg_index
    for business_info in business_list:
        company_id = business_info[0]
        business_id = int(business_info[1])
        business_name = business_info[2]
        single_business_msgs = daily_report.generate_msg(
            company_id, business_id, business_name
        )
        if single_business_msgs:
            global_msg_list.extend(single_business_msgs)
            if company_id == daily_report.fandeng_company_id:
                fandeng_msg_list.extend(single_business_msgs)
    # 所有账号的日报推送
    logging.info(f"所有业务原始消息列表：\n{data2beautify(global_msg_list)}")
    send_msg(daily_report.robot_url, global_msg_list)
    global_msg_index = 1
    # 樊登的日报推送
    logging.info(f"樊登业务原始消息列表: {data2beautify(fandeng_msg_list)}")
    send_msg(daily_report.fandeng_url, fandeng_msg_list)
    fandeng_msg_index = 1


if __name__ == "__main__":
    # everyday_push("pls", "2025-02-25")
    scheduler = BlockingScheduler()
    scheduler.add_job(everyday_push, args=["pls"], trigger="cron", hour=22, minute=5)
    scheduler.start()
